/**
 *  Copyright 2009 Henning Teek, Ingo Renner
 *  
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *  
 *  http://www.apache.org/licenses/LICENSE-2.0
 *  
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License. 
 */
package com.googlecode.chigs.repository.jpa.impl;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.annotation.Resource;

import org.apache.log4j.lf5.util.StreamUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.googlecode.chigs.domain.Annotation;
import com.googlecode.chigs.domain.Attachment;
import com.googlecode.chigs.domain.ConfigLocation;
import com.googlecode.chigs.domain.ConfigLocationGroup;
import com.googlecode.chigs.domain.ConfigPair;
import com.googlecode.chigs.domain.ConfigRevision;
import com.googlecode.chigs.domain.ConfigValue;
import com.googlecode.chigs.domain.Configuration;
import com.googlecode.chigs.domain.GenericScript;
import com.googlecode.chigs.domain.Module;
import com.googlecode.chigs.domain.Product;
import com.googlecode.chigs.domain.Revision;
import com.googlecode.chigs.domain.Version;
import com.googlecode.chigs.domain.ConfigLocation.ConfigLocationType;
import com.googlecode.chigs.repository.ChigsFacade;
import com.googlecode.chigs.repository.ConfigurationStructureCreationException;
import com.googlecode.chigs.repository.jpa.AnnotationDao;
import com.googlecode.chigs.repository.jpa.AttachmentDao;
import com.googlecode.chigs.repository.jpa.ConfigLocationDao;
import com.googlecode.chigs.repository.jpa.ConfigLocationGroupDao;
import com.googlecode.chigs.repository.jpa.ConfigPairDao;
import com.googlecode.chigs.repository.jpa.ConfigRevisionDao;
import com.googlecode.chigs.repository.jpa.ConfigValueDao;
import com.googlecode.chigs.repository.jpa.ConfigurationDao;
import com.googlecode.chigs.repository.jpa.GenericScriptDao;
import com.googlecode.chigs.repository.jpa.ModuleDao;
import com.googlecode.chigs.repository.jpa.RevisionDao;
import com.googlecode.chigs.repository.jpa.VersionDao;
import com.googlecode.chigscommons.repository.StorehouseService;
import com.googlecode.chigscommons.repository.UncheckedRuntimeException;

/**
 * @author Ingo Renner
 * @author Henning Teek
 */
@Service("chigs")
@Transactional
public class ChigsFacadeImpl implements ChigsFacade {
	private final static Logger log = LoggerFactory.getLogger(ChigsFacadeImpl.class);
	
	@Resource
	private AnnotationDao annotationDao;
	
	@Resource
	private AttachmentDao attachmentDao;
	
	@Resource
	private ConfigLocationDao configLocationDao;
	
	@Resource
	private ConfigLocationGroupDao configLocationGroupDao;
	
	@Resource
	private ConfigPairDao configPairDao;
	
	@Resource
	private ConfigurationDao configurationDao;
	
	@Resource
	private ConfigValueDao configValueDao;
	
	@Resource
	private GenericScriptDao genericScriptDao;
	
	@Resource
	private ConfigRevisionDao configRevisionDao;
	
	@Resource
	private ModuleDao moduleDao;
	
	@Resource
	private RevisionDao revisionDao;
	
	@Resource
	private VersionDao versionDao;
	
	@Autowired
	private StorehouseService storehouseService;
	
	@Override
	public List<ConfigLocation> findConfigLocationsByEmptyGroup() {
		return configLocationDao.findByEmptyConfigLocationGroup();
	}

	@Override
	public void saveConfigLocationGroup(ConfigLocationGroup group) {
		configLocationGroupDao.save(group);

		for (ConfigLocation configLocation : configLocationDao.findByConfigLocationGroup(group)) {
			configLocation.setConfigLocationGroup(null);
			configLocationDao.save(configLocation);
		}

		for (ConfigLocation configLocation : group.getConfigLocations()) {
			configLocation.setConfigLocationGroup(group);
			configLocationDao.save(configLocation);
		}
	}

	@Transactional(readOnly = true)
	@Override
	public List<Module> loadModulesByProduct(Product product) {
		List<Module> modules = new ArrayList<Module>();
		List<Version> versions = product != null ? versionDao.findByProduct(product) : new ArrayList<Version>();
		log.info("versions: " + versions + " loaded for product: " + product);
		for (Version version : versions) {
			List<Module> vmodules = moduleDao.findByVersion(version);
			log.info("modules: " + vmodules + " loaded for version: " + version);
			for (Module module : vmodules) {
				if (!modules.contains(module)) {
					modules.add(module);
				}
			}
		}
		log.info("modules: " + modules + " loaded for product: " + product);
		if (log.isDebugEnabled()) {
			for (Module module : modules) {
				log.debug("Module: " + module.toString());
				for (Revision revision : module.getRevisions()) {
					log.debug("Revision: " + revision.toString());
				}
			}
		}
		return modules;		
	}
	
	@Override
	public void saveModule(Module module) {
		moduleDao.save(module);
	}
	
	@Override
	public void saveVersion(Version version) {
		versionDao.save(version);
	}
	
	@Override
	public void saveRevision(Revision revision) {
		if (log.isDebugEnabled()) {
			log.debug("saveRevision - start");
		}

		if (!revision.isNew()) {
			// old revision will be saved for history
			Revision newRevision = new Revision();
			newRevision.setNr(revision.getNr() + 1);
			newRevision.setDescription(revision.getDescription());
			newRevision.setUser(revision.getUser());
			newRevision.setModule(revision.getModule());
			newRevision.setVersion(revision.getVersion());
			newRevision.setDependencies(new ArrayList<Module>(revision.getDependencies()));
			newRevision.setAnnotations(new ArrayList<Annotation>(revision.getAnnotations()));
			newRevision.setConfigPairs(new ArrayList<ConfigPair>(revision.getConfigPairs()));
			newRevision.setGenericScripts(new ArrayList<GenericScript>(revision.getGenericScripts()));
			newRevision.setAttachments(new ArrayList<Attachment>(revision.getAttachments()));
			
			revision = newRevision;
		}
		
		Module module = revision.getModule();
		moduleDao.saveAndFlush(module);
		
		for (Annotation annotation : revision.getAnnotations()) {
			if (annotation.isNew()) {
				annotationDao.saveAndFlush(annotation);
				if (log.isDebugEnabled()) {
					log.debug("creating new annotation: " + annotation);
				}
			}
		}

		for (ConfigPair configPair : revision.getConfigPairs()) {
			if (configPair.isNew()) {
				configPairDao.saveAndFlush(configPair);
				if (log.isDebugEnabled()) {
					log.debug("creating new config pair: " + configPair);
				}
			}
		}

		for (GenericScript genericScript : revision.getGenericScripts()) {
			if (genericScript.isNew()) {
				genericScriptDao.saveAndFlush(genericScript);
				if (log.isDebugEnabled()) {
					log.debug("creating new generic script: " + genericScript);
				}
			}
		}

		for (Attachment attachment : revision.getAttachments()) {
			if (attachment.isNew()) {
				attachmentDao.saveAndFlush(attachment);
				if (log.isDebugEnabled()) {
					log.debug("creating new attachment: " + attachment);
				}
			}
		}

		revisionDao.saveAndFlush(revision);
		
		if (log.isDebugEnabled()) {
			Module test = moduleDao.readByPrimaryKey(module.getId());
			log.debug("saveRevision - end module.getRevisions()="+test.getRevisions().toString());
		}
	}
	
	@Override
	public Revision loadLatestRevision(Module module) {
		List<Revision> revisions = revisionDao.findByModuleLatest(module);
		if (!revisions.isEmpty() && revisions.size() > 1) {
			log.error("more than one latest revision found, actual: " + revisions.size());
			throw new UncheckedRuntimeException("more than one latest revision found");
		} 
		
		return revisions.isEmpty() ? new Revision() : revisions.get(0);
	}
	
	@Override
	public void saveConfigRevision(ConfigRevision configRevision) {
		if (log.isDebugEnabled()) {
			log.debug("saveConfigRevision - start");
		}
		
		// Leere Values werden nicht gespeichert
		List<ConfigValue> configValues = configRevision.getConfigValues();
		for (Iterator<ConfigValue> iterator = configValues.iterator(); iterator.hasNext();) {
			ConfigValue configValue = iterator.next();
			if (configValue.getData() == null || "".equals(configValue.getData())) {
				iterator.remove();
			}
		}

		if (!configRevision.isNew()) {
			// old revision will be saved for history
			configRevision.setId(null);
			configRevision.setNr(configRevision.getNr() + 1);

			// copy for history
			configRevision.setRevisions(new ArrayList<Revision>(configRevision.getRevisions()));
			configRevision.setConfigValues(new ArrayList<ConfigValue>(configRevision.getConfigValues()));
		}
		
		Configuration configuration = configRevision.getConfiguration();
		configurationDao.save(configuration);

		for (ConfigValue configValue : configRevision.getConfigValues()) {
			configValueDao.save(configValue);
			if (log.isDebugEnabled()) {
				log.debug("creating new configuration value: " + configValue);
			}
		}

		configRevisionDao.save(configRevision);

		if (log.isDebugEnabled()) {
			log.debug("saveConfigRevision - end");
		}
	}
	
	@Override
	public ConfigRevision loadLatestConfigRevision(Configuration configuration) {
		List<ConfigRevision> configRevisions = configRevisionDao.findByConfigurationLatest(configuration);
		if (!configRevisions.isEmpty() && configRevisions.size() > 1) {
			throw new UncheckedRuntimeException("more than one latest config revision found");
		}
		ConfigRevision configRevision;
		
		if (configRevisions.isEmpty()) {
			configRevision = new ConfigRevision();
		} else {
			configRevision = configRevisions.get(0);
			configRevision.setRevisions(revisionDao.findByConfigRevision(configRevision));
		}
		if (log.isDebugEnabled()) {
			log.debug("loading latest config revision: " + configRevision + " for configuration: " + configuration);
			log.debug("adding revisions: " + configRevision.getRevisions());
		}
		return configRevision;
	}

	@Override
	public List<Attachment> loadAttachementsByRevison(Revision revision) {
		List<Attachment> attachments = attachmentDao.findByRevision(revision);
		if (log.isDebugEnabled()) { 
			log.debug("loading attachments: " + attachments + " for revison: " + revision);
		}
		return attachments;
	}
	
	@Override
	public List<Version> loadVersionsByProduct(Product product) {
		List<Version> versions = product != null ? versionDao.findByProduct(product) : new ArrayList<Version>();
		if (log.isDebugEnabled()) { 
			log.debug("loading versions: " + versions + " for product: " + product);
		}
		return versions;
	}
	
	@Override
	public List<Revision> loadRevisionsByModule(Module module) {
		List<Revision> revisions = module != null ? revisionDao.findByModule(module) : new ArrayList<Revision>();
		if (log.isDebugEnabled()) { 
			log.debug("loading revisions: " + revisions + " for module: " + module);
		}
		return revisions;
	}
	
	private File getTmpDir() {
		final File temp = new File("target/temp");
		if (!temp.exists()) {
			temp.mkdirs();
		}
		return temp;
	}

	@Override
	public List<ConfigRevision> loadConfigRevisionsByConfiguration(Configuration configuration) {
		return configuration != null ? configRevisionDao.findByConfiguration(configuration) : new ArrayList<ConfigRevision>();
	}

	@Override
	public List<Configuration> loadConfigurationsByProduct(Product product) {
		return configurationDao.findByProduct(product);
	}

	@Override
	public List<ConfigLocation> loadLocationsByVersion(Version version) {
		return configLocationDao.findByVersion(version.getMajor(), version.getMinor());
	}

	@Override
	public List<ConfigPair> loadConfigPairsByRevision(Revision revision) {
		return configPairDao.findByRevision(revision);
	}

	@Override
	public long getConfigurationCountByProduct(Product product) {
		return configurationDao.findByProductCount(product).get(0);
	}

	@Override
	public long getModuleCountByProduct(Product product) {
		return moduleDao.findByProductCount(product).get(0);
	}
	
	@Override
	/**
	 * Wird beim Aufruf von ZipResourceStream(final File dir, final boolean recursive) verwendet
	 * 
	 * Verwender muss sich um das löschen kümmern. Das Verzeichnis wird sonst erst bei beendigung der Application gelöscht.
	 */
	public File generateConfigurationFilestructure(ConfigRevision configRevision) throws ConfigurationStructureCreationException {
		// 1.) create Tempdir for this configuration
		final StringBuilder suffix = new StringBuilder();
		final Date now = new Date();
		final Product product = configRevision.getVersion().getProduct();

		suffix.append(product.getName())
			.append("-")
			.append(configRevision.getVersion().getVersion())
			.append("-")
			.append(now.getTime());
		final File outDir = new File(getTmpDir(), suffix.toString());
		if (!outDir.exists()) {
			outDir.mkdirs();
		} else {
			throw new ConfigurationStructureCreationException("Directory allready exists: " + outDir.getAbsolutePath());
		}
		
		// 2.) Prepare data
		// Alle konfigurierten Config Values laden, müssen noch Modulen zugeordnet werden.
		final List<ConfigValue> configuredValues = loadConfigValuesByConfigRevision(configRevision);
		final Map<Long, ConfigValue> valueMap = new HashMap<Long, ConfigValue>(configuredValues.size());
		// Zugriffsoptimierung
		for (ConfigValue cv : configuredValues) {
			log.debug("Putting ConfigValue:" + cv);
			valueMap.put(cv.getConfigPair().getId(), cv);
		}
		// Created property files
		final Map<String, OutputStream> propertyMap = new HashMap<String, OutputStream>();

		// 3.) Write out all the information by revision
		final Map<String, Boolean> headerMap = new HashMap<String, Boolean>();
		for (final Revision r : configRevision.getRevisions()) {
			final Module header = r.getModule();
			// Remember the headers written to a property file
			// get all properties
			final List<ConfigPair> allPairs = loadConfigPairsByRevision(r);
			for (ConfigPair cp : allPairs) {
				// check if they are configured and where they belong
				if (valueMap.containsKey(cp.getId())) {
					// found configured property of current module.
					if (cp.getConfigLocation().getType() != ConfigLocationType.PROPERTY_FILE) {
						throw new ConfigurationStructureCreationException("Location must be type file for propery: " + cp.getName());
					}
					final String loc = cp.getConfigLocation().getLocation();
					final String key = cp.getName();
					final String value = valueMap.get(cp.getId()).getData();
					if (log.isDebugEnabled()) {
						log.debug("generating: " + key + "=" + value + " to config location: " + loc);
					}
					// Assure that we have a proper location to write 
					if (!propertyMap.containsKey(loc)) {
						try {
							propertyMap.put(loc, new FileOutputStream(storehouseService.createFile(new File(outDir, loc))));
						} catch (IOException e) {
							e.printStackTrace();
						}
					}
					final PrintStream ps = new PrintStream(propertyMap.get(loc));
					// Do we need the header entry?
					if (!headerMap.containsKey(header.getName()+loc)){
						headerMap.put(header.getName()+loc, Boolean.TRUE);
						ps.printf("# Module %s\n", header.getName());
					}
					// Finally write property.
					ps.printf("%s=%s\n", key, value);
				}
			}
			// Check if there are attachments to write.
			final List<Attachment> attachments = loadAttachementsByRevison(r);
			for (Attachment a : attachments) {
				// Only write attachments, if they have a directory as location.
				if (a.getConfigLocation() != null && a.getConfigLocation().getType() == ConfigLocationType.DIRECTORY) {
					final String destination = outDir
						+ System.getProperty("file.separator")
						+ makeRelativePath(a.getConfigLocation().getLocation()) 
						+ System.getProperty("file.separator")
						+ a.getFilename();
					if (log.isDebugEnabled()) {
						log.debug("copy Data from: " + storehouseService.getDataFile(a.getHash()).getAbsolutePath() + " to " + destination);
					}
					try {
						FileOutputStream fos = new FileOutputStream(storehouseService.createFile(new File(destination)));
						FileInputStream fis = new FileInputStream(storehouseService.getDataFile(a.getHash()));
						StreamUtils.copyThenClose(fis, fos);
					} catch (IOException e) {
						log.error(e.getMessage(),e);
						throw new ConfigurationStructureCreationException(e.getLocalizedMessage(), e);
					}
				} else {
					// TODO Was kann man hier besser machen?
					if (log.isDebugEnabled()) {
						log.debug("Location nicht ok:" + a);				
					}
				}
			}
		}
		// All properties written, close streams.
		for ( OutputStream os : propertyMap.values()){
			try {
				os.close();
			} catch (IOException e) {
				throw new ConfigurationStructureCreationException(e.getLocalizedMessage(), e);
			}
		}
		// Create MD5 for each property file.
		try {
			for ( String  loc : propertyMap.keySet()){
				PrintStream ps = new PrintStream(new FileOutputStream(storehouseService.createFile(new File(outDir,loc + ".md5"))));
				File file = new File(outDir, loc);
				ps.printf("%s\n", storehouseService.createMD5Digest(file.getAbsolutePath()));
				ps.close();
			}
		} catch (IOException e) {
			throw new ConfigurationStructureCreationException(e.getLocalizedMessage(), e);
		}
		// 4.) Done.
		return outDir;
	}
	
	private String makeRelativePath(String dir) {
		if (dir.startsWith("/") || dir.startsWith("\\")) {
			 return dir.substring(1);
		} else {
			return dir;
		}
		
	}
	@Override
	public List<ConfigValue> loadConfigValuesByConfigRevision(ConfigRevision configRevision) {
		return configRevision != null ? configValueDao.findByConfigRevision(configRevision) : new ArrayList<ConfigValue>();
	}
}
